﻿using System;
using System.Diagnostics.Contracts;
using System.Threading;

namespace O1.Kernel.IO
{
    [ContractVerification(true)]
    public sealed class SequenceCache32 : IDisposable
    {
        private readonly int cacheSize;

        private readonly SequenceProvider<int> provider;

        private int max;

        private int value;

        private long rangeInFlight;

        public SequenceCache32(int cacheSize)
            : this(new DefaultSequenceCounter(), cacheSize)
        {
            Contract.Requires<ArgumentOutOfRangeException>(0 < cacheSize, ExceptionText.SequenceCacheMustBe1OrLarger);
        }

        public SequenceCache32(SequenceProvider<int> provider, int cacheSize)
        {
            Contract.Requires<ArgumentNullException>(provider != null);
            Contract.Requires<ArgumentOutOfRangeException>(0 < cacheSize, ExceptionText.SequenceCacheMustBe1OrLarger);

            this.cacheSize = cacheSize;
            this.provider = provider;
            this.max = int.MinValue;
        }

        public void Dispose()
        {
            this.provider.Dispose();
        }

        public int NextId()
        {
            for (;;)
            {
                var myMax = this.max;
                Thread.MemoryBarrier();

                var myValue = this.value;
                while (Interlocked.CompareExchange(ref this.value, myValue + 1, myValue) != myValue)
                {
                    myValue = this.value;
                }

                myValue++;
                if (myValue < myMax)
                {
                    return myValue;
                }

                if (Interlocked.CompareExchange(ref this.rangeInFlight, 1, 0) != 0)
                {
                    var wait = new SpinWait();
                    while (Interlocked.CompareExchange(ref this.rangeInFlight, 0, 0) == 1)
                    {
                        wait.SpinOnce();
                    }

                    continue;
                }

                try
                {
                    var sequence = this.provider.Reserve(this.cacheSize);
                    this.value = sequence;
                    Thread.MemoryBarrier();

                    this.max = unchecked((int)Math.Min((long)int.MaxValue, (long)(sequence + this.cacheSize)));
                    Thread.MemoryBarrier();
                    return sequence;
                }
                finally
                {
                    this.rangeInFlight = 0;
                }
            }
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this.provider != null);
        }

        private class DefaultSequenceCounter : SequenceProvider<int>
        {
            private int currentValue;

            public override int Reserve(int rangeSize)
            {
                var value = this.currentValue;
                this.currentValue = value + rangeSize;
                return value;
            }
        }
    }
}